﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using Fabrication.DataStructures;
using Fabrication.Exceptions;
using Fabrication.Extensions;
using Fabrication.ValueProviders;

namespace Fabrication
{
    public static class GenericRuleExtensions
    {
        public static IBuilderOptions<T> Use<T, TParam>(this PropertySelector<T, TParam> propertySelector, TParam value)
        {
            return Use(propertySelector, new FunctionValueProvider<T, TParam>(x => value));
        }

        public static IBuilderOptions<T> Use<T, TParam>(this PropertySelector<T, TParam> propertySelector, Expression<Func<T, TParam>> valueProvider)
        {
            var dependencies = GetPropertyDependencies(propertySelector.PropertyName, valueProvider);
            propertySelector.BuilderOptions.AddDependencies(typeof(T), typeof(TParam), propertySelector.PropertyName, dependencies);
            //propertySelector.BuilderOptions.AddInternalLink(typeof(T), typeof(TParam), propertySelector.PropertyName, dependencies.ToArray());
            return Use(propertySelector, new FunctionValueProvider<T, TParam>(valueProvider.Compile()));
        }

        public static IBuilderOptions<T> Use<T, TParam>(this PropertySelector<T, TParam> propertySelector, IValueProvider valueProvider)
        {
            var builderOptions = propertySelector.BuilderOptions;
            var key = new TypedPropertyKey(typeof (T), propertySelector.PropertyName);
            builderOptions.AddPropertyValueProvider(key, valueProvider);
            return builderOptions;
        }

        public static IBuilderOptions<T> To<T, TParam>(this LinkedPropertySelector<T, TParam> targetPropertySelector, Expression<Func<T, TParam>> member)
        {
            var sourceProperty = member.GetPropertyName();
            if (sourceProperty == targetPropertySelector.PropertyName)
                throw new LinkedPropertyException(string.Format("Cannot link property '{0}' to itself", sourceProperty));

            var builderOptions = targetPropertySelector.BuilderOptions;
            builderOptions.AddInternalLink(typeof(T), typeof(TParam), sourceProperty, targetPropertySelector.PropertyName);
            return builderOptions;
        }

        public static IBuilderOptions<T> PickFrom<T, TParam>(this PropertySelector<T, TParam> propertySelector, params TParam[] values)
        {
            return PickFrom(propertySelector, values.ToList());
        }

        public static IBuilderOptions<T> PickFrom<T, TParam>(this PropertySelector<T, TParam> propertySelector, IEnumerable<TParam> values)
        {
            var numberOfValues = values.Count();
            if (numberOfValues == 0)
                throw new ValueProviderException(string.Format("Pick list cannot be empty for property '{0}'", propertySelector.PropertyName));

            var provider = new FunctionValueProvider<T, TParam>(x => values.ElementAt(RandomNumberGenerator.GetInteger(0, numberOfValues - 1)));
            return Use(propertySelector, provider);
        }

        public static IBuilderOptions<T> PickFromDistinct<T, TParam>(this PropertySelector<T, TParam> propertySelector, params TParam[] values)
        {
            return PickFromDistinct(propertySelector, values.ToList());
        }

        public static IBuilderOptions<T> PickFromDistinct<T, TParam>(this PropertySelector<T, TParam> propertySelector, IEnumerable<TParam> values)
        {
            return Use(propertySelector, new DistinctValueProvider<TParam>(values));
        }

        public static IBuilderOptions<T> PickFromAll<T, TParam>(this PropertySelector<T, TParam> propertySelector, params TParam[] values)
        {
            return PickFromAll(propertySelector, values.ToList());
        }

        public static IBuilderOptions<T> PickFromAll<T, TParam>(this PropertySelector<T, TParam> propertySelector, IEnumerable<TParam> values)
        {
            return Use(propertySelector, new AllValueProvider<TParam>(values));
        }

        public static MappedPropertySelector<T, TSource, TTarget> Map<T, TSource, TTarget>(this PropertySelector<T, TTarget> targetPropertySelector, Expression<Func<T, TSource>> sourceMember)
        {
            if (sourceMember.GetPropertyName() == targetPropertySelector.PropertyName)
                throw new LinkedPropertyException(string.Format("Cannot map property '{0}' to itself.", targetPropertySelector.PropertyName));

            return new MappedPropertySelector<T, TSource, TTarget>(targetPropertySelector, sourceMember);
        }

        public static DoubleMappedPropertySelector<T, TSource1, TSource2, TTarget> Map<T, TSource1, TSource2, TTarget>(this PropertySelector<T, TTarget> targetPropertySelector, Expression<Func<T, TSource1>> sourceMember1, Expression<Func<T, TSource2>> sourceMember2)
        {
            var targetPropertyName = targetPropertySelector.PropertyName;
            if (sourceMember1.GetPropertyName() == targetPropertyName || sourceMember2.GetPropertyName() == targetPropertyName)
                throw new LinkedPropertyException(string.Format("Cannot map property '{0}' to itself.", targetPropertyName));

            return new DoubleMappedPropertySelector<T, TSource1, TSource2, TTarget>(targetPropertySelector, sourceMember1, sourceMember2);
        }

        public static IBuilderOptions<T> UseSequence<T, TParam>(this PropertySelector<T, TParam> propertySelector, params TParam[] items)
        {
            var queue = new Sequence<TParam>(items);
            return Use(propertySelector, x => queue.Next());
        }

        public static IBuilderOptions<T> UseSequence<T, TParam>(this PropertySelector<T, TParam> propertySelector, IEnumerable<TParam> items)
        {
            return UseSequence(propertySelector, items.ToArray());
        }

        private static IEnumerable<string> GetPropertyDependencies<T, TParam>(string propertyName, Expression<Func<T, TParam>> expression)
        {
            return GetPropertyDependencies(propertyName, expression.Body);
        }

        private static IEnumerable<string> GetPropertyDependencies(string propertyName, Expression expression)
        {
            var dependencies = new List<string>();

            var binaryExpression = expression as BinaryExpression;
            if (binaryExpression != null)
            {
                dependencies.AddRange(GetPropertyDependencies(propertyName, binaryExpression.Left));
                dependencies.AddRange(GetPropertyDependencies(propertyName, binaryExpression.Right));
            }
            var memberExpression = expression as MemberExpression;
            if (memberExpression != null)
            {
                if (memberExpression.Member.Name == propertyName)
                    throw new SelfReferenceException(string.Format("Property '{0}' cannot reference itself.", propertyName));
                
                dependencies.Add(memberExpression.Member.Name);
            }

            return dependencies;
        }
    }
}